Testing is often seen as a tedious process. It's extra code you have to write, and in some cases, to be honest, it's not needed. But every developer should know at least the basics of testing. It increases confidence in the products they build, and for most companies, it's a requirement.
In the React world, there is an amazing library called the react-testing-library which helps you test your React Apps more efficiently. You use it with Jest.
In this article, we will see the 8 simple steps you can take to start testing your React Apps like a boss.
- Prerequisites
- Basics
- What is React Testing Library?
- 1. How to create a test snapshot?
- 2. Testing DOM elements
- 3. Testing events
- 4. Testing asynchronous actions
- 5. Testing React Redux
- 6. Testing React Context
- 7. Testing React Router
- 8. Testing HTTP Request
- Final Thoughts
- Next Steps
Prerequisites
This tutorial assumes that you have at least a basic understanding of React. I will focus only on the testing part.
And to follow along, you have to clone the project by running in your terminal:
git clone https://github.com/ibrahima92/prep-react-testing-library-guide
Next, run:
yarn
Or, if you use NPM:
npm install
And that's it! Now let's dive into some basics.
Basics
Some key things will be used a lot in this article, and understanding their role can help you with your understanding.
it or test: describes the test itself. It takes as parameters the name of the test and a function that holds the tests.
expect: the condition that the test needs to pass. It will compare the received parameter to a matcher.
a matcher: a function that is applied to the expected condition.
render: the method used to render a given component.
import React from 'react'
import {render} from '@testing-library/react'
import App from './App'
it('should take a snapshot', () => {
const { asFragment } = render(<App />)
expect(asFragment(<App />)).toMatchSnapshot()
})
});
As you can see, we describe the test with it, then, use render to display the App component and expect that asFragment(<App />) matches toMatchSnapshot() (the matcher provided by jest-dom).
By the way, the render method returns several methods we can use to test our features. We also used destructuring to get the method.
That being said, let's move on and learn more about the React Testing Library in the next section.
What is the React Testing Library?
The React Testing Library is a very light-weight package created by Kent C. Dodds. It's a replacement for Enzyme and provides light utility functions on top of react-dom and react-dom/test-utils.
The React Testing Library is a DOM testing library, which means that instead of dealing with instances of rendered React components, it handles DOM elements and how they behave in front of real users.
It's a great library, it's (relatively) easy to start using, and it encourages good testing practices. Note – you can also use it without Jest.
"The more your tests resemble the way your software is used, the more confidence they can give you."
So, let's start using it in the next section. By the way, you don't need to install any packages, since create-react-app comes with the library and its dependencies.
1. How to create a test snapshot
A snapshot, as the name suggests, allows us to save the snapshot of a given component. It helps a lot when you update or do some refactoring, and want to get or compare the changes.
Now, let's take a snapshot of the App.js file.
App.test.js
import React from 'react'
import {render, cleanup} from '@testing-library/react'
import App from './App'
afterEach(cleanup)
it('should take a snapshot', () => {
const { asFragment } = render(<App />)
expect(asFragment(<App />)).toMatchSnapshot()
})
});
To take a snapshot, we first have to import render and cleanup. These two methods will be used a lot throughout this article.
render, as you might guess, helps to render a React component. And cleanup is passed as a parameter to afterEach to just clean up everything after each test to avoid memory leaks.
Next, we can render the App component with render and get back asFragment as a returned value from the method. And finally, make sure that the fragment of the App component matches the snapshot.
Now, to run the test, open your terminal and navigate to the root of the project and run the following command:
yarn test
Or, if you use npm:
npm test
As a result, it will create a new folder __snapshots__ and a file App.test.js.snap in the src which will look like this:
App.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Take a snapshot should take a snapshot 1`] = `
<DocumentFragment>
<div class="App">
<h1>Testing</h1>
</div>
</DocumentFragment>
`;
And if you make another change in App.js, the test will fail, because the snapshot will no longer match the condition. To make it passes, just press u to update it. And you'll have the updated snapshot in App.test.js.snap.
Now, let's move on and start testing our elements.
2. Testing DOM elements
To test our DOM elements, we first have to look at the TestElements.js file.
TestElements.js
import React from 'react'
const TestElements = () => {
const [counter, setCounter] = React.useState(0)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button>
<button disabled data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestElements
Here, the only thing you have to retain is data-testid. It will be used to select these elements from the test file. Now, let's write the unit test:
Test if the counter is equal to 0:
TestElements.test.js
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import TestElements from './TestElements'
afterEach(cleanup);
it('should equal to 0', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('counter')).toHaveTextContent(0)
});
As you can see, the syntax is quite similar to the previous test. The only difference is that we use getByTestId to select the necessary elements (remember the data-testid) and check if it passed the test. In others words, we check if the text content <h1 data-testid="counter">{ counter }</h1> is equal to 0.
Test if the buttons are enabled or disabled:
TestElements.test.js (add the following code block to the file)
it('should be enabled', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('button-up')).not.toHaveAttribute('disabled')
});
it('should be disabled', () => {
const { getByTestId } = render(<TestElements />);
expect(getByTestId('button-down')).toBeDisabled()
});
Here, as usual, we use getByTestId to select elements and check for the first test if the button has a disabled attribute. And for the second, if the button is disabled or not.
And if you save the file or run again in your terminal yarn test, the test will pass.
Congrats! Your first test has passed!

Now, let's learn how to test an event in the next section.
3. Testing events
Before writing our unit tests, let's first check what the TestEvents.js looks like.
TestEvents.js
import React from 'react'
const TestEvents = () => {
const [counter, setCounter] = React.useState(0)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={() => setCounter(counter + 1)}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestEvents
Now, let's write the tests.
Test if the counter increments and decrements correctly when we click on buttons:
TestEvents.test.js
import React from 'react';
import { render, cleanup, fireEvent } from '@testing-library/react';
import TestEvents from './TestEvents'
afterEach(cleanup);
it('increments counter', () => {
const { getByTestId } = render(<TestEvents />);
fireEvent.click(getByTestId('button-up'))
expect(getByTestId('counter')).toHaveTextContent('1')
});
it('decrements counter', () => {
const { getByTestId } = render(<TestEvents />);
fireEvent.click(getByTestId('button-down'))
expect(getByTestId('counter')).toHaveTextContent('-1')
});
As you can see, these two tests are very similar except the expected text content.
The first test fires a click event with fireEvent.click() to check if the counter increments to 1 when the button is clicked.
And the second one checks if the counter decrements to -1 when the button is clicked.
fireEvent has several methods you can use to test events, so feel free to dive into the documentation to learn more.
Now that we know how to test events, let's move on and learn in the next section how to deal with asynchronous actions.
4. Testing asynchronous actions
An asynchronous action is something that can take time to complete. It can be an HTTP request, a timer, and so on.
Now, let's check the TestAsync.js file.
TestAsync.js
import React from 'react'
const TestAsync = () => {
const [counter, setCounter] = React.useState(0)
const delayCount = () => (
setTimeout(() => {
setCounter(counter + 1)
}, 500)
)
return (
<>
<h1 data-testid="counter">{ counter }</h1>
<button data-testid="button-up" onClick={delayCount}> Up</button>
<button data-testid="button-down" onClick={() => setCounter(counter - 1)}>Down</button>
</>
)
}
export default TestAsync
Here, we use setTimeout() to delay the incrementing event by 0.5s.
Test if the counter is incremented after 0.5s:
TestAsync.test.js
import React from 'react';
import { render, cleanup, fireEvent, waitForElement } from '@testing-library/react';
import TestAsync from './TestAsync'
afterEach(cleanup);
it('increments counter after 0.5s', async () => {
const { getByTestId, getByText } = render(<TestAsync />);
fireEvent.click(getByTestId('button-up'))
const counter = await waitForElement(() => getByText('1'))
expect(counter).toHaveTextContent('1')
});
To test the incrementing event, we first have to use async/await to handle the action because, as I said earlier, it takes time to complete.
Next, we use a new helper method getByText(). This is similar to getByTestId(), except that getByText() selects the text content instead of id or data-testid.
Now, after clicking to the button, we wait for the counter to be incremented with waitForElement(() => getByText('1')). And once the counter incremented to 1, we can now move to the condition and check if the counter is effectively equal to 1.
That being said, let's now move to more complex test cases.
Are you ready?
